Securing responses from Actionable Messages
Table of contents:
In my previous post I guided you through a list of steps required to build, send and handle response from Adaptive Cards as Actionable Messages in Outlook. Let me now tell you, how to secure the response.
Basically, the response from Adaptive Card sent as Actionable Message can be easily “hacked”. In its definition we define the “Url” attribute that tells the form where to send data. If a message is forwarded to another person in tenant or if someone builds a form that submits data to the same endpoint as our message, it will accept anything when not having any kind of verification mechanism. Therefore our solution should be prepared to identify source and terminate further execution in case of attempted fraud.
If the card is used for real enterprise purposes, than you could use mechanism called “signing cards”, so that using your private key you need to sign payload and then using public verify the response. This uses DKIM and SPF standards: https://docs.microsoft.com/en-us/outlook/actionable-messages/security-requirements#action-authorization-header. What if we need something simpler?
See it in action!
Action-Authorization header to the rescue
Each response from Actionable Message contains a header key Action-Authorization. It looks like a regular Bearer token, but it’s not. This is JSON Web Signature (JWS).
Important! Action-Authorization Bearer token cannot be used to call eg. GraphAPI to get user’s profile. It will always return 401, because this is not a valid AAD Bearer token.
Contents of the key are base64 encoded JSON objects. Once you split it using “.”, there are three parts (according to RFC7515 standard):
- JWS header
- JWS payload
- JWS signature
JWS header
Decode it using the following expression: decodeBase64(split(replace(triggerOutputs()['headers']?['Action-Authorization'], 'Bearer ', ''), '.')?[0])
{"typ":"JWT","alg":"RS256","kid":"RfKI6h2uhxp6rHwux79UcFh","x5t":"RfKI6h2uhxp6rHwux79UcFh","issloc":"AM0PR10MB","sqid":63734169909382}
Defines the JWT (JSON Web Token) type used. This one is not very much useful for validation purposes.
JWS payload
Decode it using the following expression: decodeBase64(concat(split(replace(triggerOutputs()['headers']?['Action-Authorization'], 'Bearer ', ''), '.')?[1], '=='))
{ "iat":1598827285, "ver":"STI.ExternalAccessToken.V1", "appid":"48af08dc-f6d2-435f-b2a7-069abd99c086", "sub":"WHO SUBMITTED CARD", "appidacr":"2", "acr":"0", "tid":"32443e19-8cfe-4e6b-a3fe-e75c0e09c7a8", "sender":"WHO SENT CARD", "oid":"e4e6b40f-f698-403e-bc8e-252a2d52f22c", "iss":"https://substrate.office.com/sts/", "aud":"https://prod-135.westeurope.logic.azure.com", "exp":1598828185, "nbf":1598827285 }
Basically when sending an Actionable Message to a user and waiting for their response you should store basic information somewhere to compare it once response is received, for example: CDS or SharePoint. Then you could compare:
- appid – this is the ID of the app that issued token, in case of Outlook it should always be 48af08dc-f6d2-435f-b2a7-069abd99c086.
- sub – this is going to be an e-mail of user who clicked to submit the response.
- sender – this is going to be an e-mail of the account used to send Actionable Message.
- aud – url of the Logic Apps instance used to execute action.
- iat – when payload was signed (UNIX timestamp)
- exp – when signature expires (UNIX timestamp)
So in this case you can compare if appid and sub values are as expected. More: https://docs.microsoft.com/en-us/outlook/actionable-messages/security-requirements#verifying-that-requests-come-from-microsoft.
CorrelationId and originator in Adaptive Card
What can also be used, even harder to hack, is to use CorrelationId + originator attributes in Adaptive Cards. You can generate your custom GUID (eg. using guid()
expression in Power Automate), save it in CDS or SharePoint, then set it as a value of the CorrelationId attribute:
{ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.0", "hideOriginalBody": true, "originator": "f6f1d226-d6bb-409e-853e-0747fd61234", "correlationId": "@{guid()}",
Once the response is received, simply compare values of the originator property with the registered provider guid and saved CorrelationId with Card-Correlation-Id value from header: triggerOutputs()['headers']?['Card-Correlation-Id']
(source: https://docs.microsoft.com/en-us/outlook/actionable-messages/adaptive-card#additional-properties-on-the-adaptivecard-type).
Service-specific tokens
What Microsoft is also recommending is to add custom generated tokens added to the target URL defined in Adaptive Card’s actions, eg. https://contoso.com/approve?requestId=abc&serviceToken=a1b2c3d4e5
More on this: https://docs.microsoft.com/en-us/outlook/actionable-messages/security-requirements#verifying-the-identity-of-the-user.
I hope that the above steps will help you build your Actionable Messages and Adaptive Cards based solution and secure them properly. If you have questions, feel free to ask them in the comments below.
farissi jaafar
Hi Tomas
i receive this message
The remote endpoint returned an error (HTTP 400). Please try again later.
i’m working with this tuto
https://www.bythedevs.com/post/multi-line-approvals-with-adaptive-cards-outlook-and-power-automate
thanks for your help
Tomasz Poszytek
Try following my steps: https://poszytek.eu/en/microsoft-en/office-365-en/powerautomate-en/adaptive-cards-in-outlook-ultimate-guide/
oliver
hi Tomasz, I am getting HTTP 400 error, the address is working when I tried it in Postman, so is there other reason that might be causing it?
please advise, thank you
{
“type”: “Action.Http”,
“title”: “Approve”,
“method”: “POST”,
“style”: “positive”,
“id”: “RequestApprove”,
“body”: “{\n\”response\”:\”{{ApproveValue.value}}\”,\n\”requestid\”:\”{{@{triggerOutputs()?[‘body/ID’]}}}\”\n}”,
“url”: “https://prod-166.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxQXUz44Jo87CEiyevvW5QFs4FDh60dd0fluNo”,
“headers”: [
{
“name”: “Authorization”,
“value”: “”
}
]
}
oliver
I solved this! 🙂
for HTTP 400 error, it is most likely an issue with the JSON payload, I had a type: “integer”, after changing it to “string”, response was successful. 😉
Tomasz Poszytek
I am glad you sorted that out! Good luck 🙂
Priya
Hi Tomaz, Great Post. Can you please tell me how to retrieve values from JWS payload to compare data. The decodeBase64() returns following format.
“{\”iat\”:xxx,\”ver\”:\”xxx\”,\”appid\”:\”xxx\”,\”sub\”:\”xxx\”,\”appidacr\”:\”x\”,\”acr\”:\”x\”,\”tid\”:\”xxx\”,\”sender\”:\”xxx\”,\”oid\”:\”xxx\”,\”iss\”:\”xxx\”,\”aud\”:\”xxx\”,\”exp\”:xxx,\”nbf\”:xxx}”
Thanks
Priya
Solved. The parse Json schema which I had typed was wrong. Works fine now. All thanks to your blog.
Anna
Hi Tomasz,
I get the following error –
‘The template language function ‘decodeBase64’ was invoked with a parameter that is not valid. The value cannot be decoded from base64 representation.
Please suggest.
Regards
Anna
Hi, in my case sometimes
decodeBase64(concat(split(replace(triggerOutputs()[‘headers’]?[‘Action-Authorization’], ‘Bearer ‘, ”), ‘.’)?[1], ‘==’)) – this works and gives error for below expression
decodeBase64(split(replace(triggerOutputs()[‘headers’]?[‘Action-Authorization’], ‘Bearer ‘, ”), ‘.’)?[1]) – this work for some and gives error for above expression
Please suggest.
Regards,
Tomasz Poszytek
Basically you can just try to first use Compose action and put there split(replace(triggerOutputs()[‘headers’]?[‘Action-Authorization’], ‘Bearer ‘, ”), ‘.’) expression, so you will see what tokens split will return. Then investigate the second one. It should be almost a valid base64 string, just missing the ‘==’ characters at the end.
Bernardo
I am having the exact same issue as Anna.
It used to work fine.
Dinos
I face the same issue. The string generated from this
split(replace(triggerOutputs()[‘headers’]?[‘Action-Authorization’], ‘Bearer ‘, ”), ‘.’)?[1]
can be decoded using https://www.base64decode.org/ but the power automate flow returns:
‘The template language function ‘decodeBase64′ was invoked with a parameter that is not valid. The value cannot be decoded from base64 representation.’.
Tomasz Poszytek
Unfortunately I haven’t found a solution to this yet 🙁
mohammad
when Decoding the JWS with the function you added there the result contain always a symbol in the end of the “nbf” part and its missing the closure “}” it looks like this “nbf”:161467939�
Tomasz Poszytek
This technology is being updated and changed recently. That character looks like if base64 decode was not really successful.
Willian Alves
The issue seems to the the decobase64 function of power automate. I seen people report that its expects the input to be divisible by 4, so it gives the error in the cases where it doesn’t. Other decoders such as the jwt decoder.org automatically append an = to the end the code until is it divisible by 4. Some have used expressions in PA to get around that issue.
mod(length(variables(‘X’)) == 0, if not, append ‘=’ to the string until it is
I have also had this problem where decoder you posted worked for some user but not others.
The thread can be found here.
https://powerusers.microsoft.com/t5/Building-Flows/base64ToString-does-not-work-as-expected-in-each-case/m-p/628565
OBS. as far as I know the base64tostring and decodebase64 work the same.
Tomasz Poszytek
oh, I didn’t know that. Thanks for sharing, that’s very valuable finding! 🙂
Syed
Hi,
Do you have any sample to submit star rating response with comments box.
Tomasz Poszytek
This is not an oob control. So it requires a custom AC provider. Unfortunately not, I don’t have any example.
The only idea oob I have is to use radio button control, where each radio is different rating.
Rating can be added via extensibility feature of AC: https://docs.microsoft.com/en-us/adaptive-cards/sdk/rendering-cards/net-html/extensibility and the oob is still under consideration: https://portal.productboard.com/adaptivecards/1-adaptive-cards-features/c/8-rating-control.
Paul A Bedford
Hi Thomasz – great post – however, i am struggling to get the JWT fields extracted using the above decode64 functions you show above. Could you provide me more details for this?
Thank you.
Regards
Paul
Tomasz Poszytek
What actually are you missing?
Serge
Hi Tomasz,
Have you ever tried to use a web api to handle the card’s actions?
I can view my adaptive card in outlook (the card was sent with Power Automate).
But outlook always shows “The remote endpoint returned an error (HTTP 400). Please try again later.” when interacting with the card (refresh or button clicks).
When testing with Postman, the api returns properly the adaptive card.
Thanks.
Serge
Tomasz Poszytek
So the API returns a card, that is used to refresh existing card in Outlook?
Serge
Hi Tomasz,
Thank you for taking the time to reply to my post.
The answer to your question is Yes.
When the email is opened, Outlook post a refresh request to the api.
The api itself is very simple as I’m just trying to have the mechanism to work: it simply returns a dummy card (with a simple image).
I can send you the dummy refresh card if you are interested, but there is no value in there.
It goes like this:
{
“$schema”: “http://adaptivecards.io/schemas/adaptive-card.json”,
“type”: “AdaptiveCard”,
“version”: “1.0”,
“originator”: “xxx Actionable Email Developer Dashboard xxx”,
“hideOriginalBody”: true,
“body”: [
/* image here */
]
}
Serge
Tomasz Poszytek
You’re using the auto-invoke http action to trigger that refresh on opening the card? If so, be sure the web api doesn’t take long to response, because there are undocumented timeouts: https://poszytek.eu/en/microsoft-en/office-365-en/powerautomate-en/timeouts-in-actionable-messages.
Serge
Hi Tomasz,
Thank You for your time.
Affirmative: the api simply returns a new very simple card when the user clicks a button in Outlook.
Serge
Tomasz Poszytek
Check the timeouts. Possibly api takes too much time to respond? https://poszytek.eu/en/microsoft-en/office-365-en/powerautomate-en/timeouts-in-actionable-messages/.
Serge
I am sure it is not the 2 secs limit, because it is behaving exactly the same way with button clicks.
Also I’ve noticed that the api function is not being hit at all.
Tomasz Poszytek
You can try to install the Actionable Messages Debugger (https://appsource.microsoft.com/en-us/product/office/wa104381686?tab=overview) to see if the code of a card is correctly interpreted in Outlook. Also, be sure that your request contains the headers, as described here: https://poszytek.eu/en/microsoft-en/office-365-en/powerautomate-en/adaptive-cards-in-outlook-ultimate-guide/#Step_2_define_actions – both Authorization and Content-Type.
Neil
Hi Thomasz,
I am using Actionable Messages for the Microsoft D365 Approval Engine to send emails and response on the mails it self. Some user are getting a “Failed to Validate the signature of the actionable message card” when they try to take action on both Desktop Client and Phone. Could you let me know what the issue could be?
Awaiting your reply!
Best,
Neil
Tomasz Poszytek
It is really difficult to say. I don’t have experience with D365 Approval engine..
Barry
Realise this has been awhile, this when Decode Authorization Bearer Payload – I receive this error now (was working a few days ago.
InvalidTemplate
Unable to process template language expressions in action ‘Decode_Authorization_Bearer_Payload’ inputs at line ‘0’ and column ‘0’: ‘The template language function ‘decodeBase64′ was invoked with a parameter that is not valid. The value cannot be decoded from base64 representation.’.
Tomasz Poszytek
Compare to what decoded value looks like with your expressions. Maybe something changed in the payload.